5.4.3 方法集
方法集定义了接口的接受规则。看一下代码清单 5-36 所示的代码,有助于理解方法集在接口中的重要角色。
代码清单5-36 listing36.go
01 // 这个示例程序展示Go语言里如何使用接口
02 package main
03
04 import (
05 "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16 name string
17 email string
18 }
19
20 // notify是使用指针接收者实现的方法
21 func (u *user) notify() {
22 fmt.Printf("Sending user email to %s<%s>\n",
23 u.name,
24 u.email)
25 }
26
27 // main是应用程序的入口
28 func main() {
29 // 创建一个user类型的值,并发送通知
30 u := user{"Bill", "bill@email.com"}
31
32 sendNotification(u)
33
34 // ./listing36.go:32: 不能将u(类型是user)作为
35 // sendNotification的参数类型notifier:
36 // user类型并没有实现notifier
37 // (notify方法使用指针接收者声明)
38 }
39
40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43 n.notify()
44 }
代码清单5-36中的程序虽然看起来没问题,但实际上却无法通过编译。在第10行中,声明了一个名为 notifier
的接口,包含一个名为 notify
的方法。第15行中,声明了名为 user
的实体类型,并通过第21行中的方法声明实现了 notifier
接口。这个方法是使用 user
类型的指针接收者实现的。
代码清单5-37 listing36.go:第40行到第44行
40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43 n.notify()
44 }
在代码清单5-37的第42行,声明了一个名为 sendNotification
的函数。这个函数接收一个 notifier
接口类型的值。之后,使用这个接口值来调用 notify
方法。任何一个实现了 notifier
接口的值都可以传入 sendNotification
函数。现在让我们来看一下 main
函数,如代码清单5-38所示。
代码清单5-38 listing36.go:第28行到第38行
28 func main() {
29 // 创建一个user类型的值,并发送通知
30 u := user{"Bill", "bill@email.com"}
31
32 sendNotification(u)
33
34 // ./listing36.go:32: 不能将u(类型是user)作为
35 // sendNotification的参数类型notifier:
36 // user类型并没有实现notifier
37 // (notify方法使用指针接收者声明)
38 }
在 main
函数里,代码清单5-38的第30行,创建了一个 user
实体类型的值,并将其赋值给变量u。之后在第32行将u的值传入 sendNotification
函数。不过,调用 sendNotification
的结果是产生了一个编译错误,如代码清单5-39所示。
代码清单5-39 将 user
类型的值存入接口值时产生的编译错误
./listing36.go:32: 不能将u(类型是user)作为sendNotification的参数类型notifier:
user类型并没有实现notifier(notify方法使用指针接收者声明)
既然 user
类型已经在第21行实现了 notify
方法,为什么这里还是产生了编译错误呢?让我们再来看一下那段代码,如代码清单5-40所示。
代码清单5-40 listing36.go:第08行到第12行,第21行到第25行
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
21 func (u *user) notify() {
22 fmt.Printf("Sending user email to %s<%s>\n",
23 u.name,
24 u.email)
25 }
代码清单5-40展示了接口是如何实现的,而编译器告诉我们 user
类型的值并没有实现这个接口。如果仔细看一下编译器输出的消息,其实编译器已经说明了原因,如代码清单 5-41所示。
代码清单5-41 进一步查看编译器错误
(notify method has pointer receiver)
要了解用指针接收者来实现接口时为什么 user
类型的值无法实现该接口,需要先了解 方法集 。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
让我们先解释一下Go语言规范里定义的方法集的规则,如代码清单5-42所示。
代码清单5-42 规范里描述的方法集
Values Methods Receivers
-----------------------------------------------
T (t T)
*T (t T) and (t *T)
代码清单5-42展示了规范里对方法集的描述。描述中说到, T
类型的值的方法集只包含值接收者声明的方法。而指向 T
类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。从值的角度看这些规则,会显得很复杂。让我们从接收者的角度来看一下这些规则,如代码清单5-43所示。
代码清单5-43 从接收者类型的角度来看方法集
Methods Receivers Values
-----------------------------------------------
(t T) T and *T
(t *T) *T
代码清单5-43展示了同样的规则,只不过换成了接收者的视角。这个规则说,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。现在再看一下代码清单5-36所示的代码,就能理解出现编译错误的原因了,如代码清单5-44所示。
代码清单5-44 listing36.go:第28行到第38行
28 func main() {
29 // 使用user类型创建一个值,并发送通知
30 u := user{"Bill", "bill@email.com"}
31
32 sendNotification(u)
33
34 // ./listing36.go:32: 不能将u(类型是user)作为
35 // sendNotification的参数类型notifier:
36 // user类型并没有实现notifier
37 // (notify方法使用指针接收者声明)
38 }
我们使用指针接收者实现了接口,但是试图将 user
类型的值传给 sendNotification
方法。代码清单5-44的第30行和第32行清晰地展示了这个问题。但是,如果传递的是 user
值的地址,整个程序就能通过编译,并且能够工作了,如代码清单5-45所示。
代码清单5-45 listing36.go:第28行到第35行
28 func main() {
29 // 使用user类型创建一个值,并发送通知
30 u := user{"Bill", "bill@email.com"}
31
32 sendNotification(&u)
33
34 // 传入地址,不再有错误
35 }
在代码清单5-45里,这个程序终于可以编译并且运行。因为使用指针接收者实现的接口,只有 user
类型的指针可以传给 sendNotification
函数。
现在的问题是,为什么会有这种限制?事实上,编译器并不是总能自动获得一个值的地址,如代码清单5-46所示。
代码清单5-46 listing46.go
01 // 这个示例程序展示不是总能
02 // 获取值的地址
03 package main
04
05 import "fmt"
06
07 // duration是一个基于int类型的类型
08 type duration int
09
10 // 使用更可读的方式格式化duration值
11 func (d *duration) pretty() string {
12 return fmt.Sprintf("Duration: %d", *d)
13 }
14
15 // main是应用程序的入口
16 func main() {
17 duration(42).pretty()
18
19 // ./listing46.go:17: 不能通过指针调用duration(42)的方法
20 // ./listing46.go:17: 不能获取duration(42)的地址
21 }
代码清单5-46所示的代码试图获取duration类型的值的地址,但是获取不到。这展示了不能总是获得值的地址的一种情况。让我们再看一下方法集的规则,如代码清单5-47所示。
代码清单5-47 再看一下方法集的规则
Values Methods Receivers
-----------------------------------------------
T (t T)
*T (t T) and (t *T)
Methods Receivers Values
-----------------------------------------------
(t T) T and *T
(t *T) *T
因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法。